Skip to content

KAFKA-19871 - Add partition support to TestRecord#22612

Open
sebastienviale wants to merge 4 commits into
apache:trunkfrom
sebastienviale:KAFKA-19871-Multipartition-TTDriver-Add-Partion-TestRecord
Open

KAFKA-19871 - Add partition support to TestRecord#22612
sebastienviale wants to merge 4 commits into
apache:trunkfrom
sebastienviale:KAFKA-19871-Multipartition-TTDriver-Add-Partion-TestRecord

Conversation

@sebastienviale

@sebastienviale sebastienviale commented Jun 18, 2026

Copy link
Copy Markdown
Contributor

Motivation

As part of KIP-1238: Multi-partition support in
TopologyTestDriver.

TestRecord needs to carry partition information so that records produced
and consumed by TopologyTestDriver can expose the partition they belong
to. This is a prerequisite for supporting multi-partition testing with
TopologyTestDriver.

Changes

This PR updates TestRecord by:

  • adding a partition field;
  • adding constructors that allow specifying the partition explicitly;
  • preserving existing constructors, which continue to default the
    partition to -1 (unspecified);
  • including the partition in equals(), hashCode(), and toString();
  • adding equalsIgnorePartition() for tests that do not care about the
    partition.

Compatibility

Existing TestRecord constructors remain unchanged and continue to assign
partition = -1.

Since no component currently sets a partition value, records created
through existing APIs continue to carry partition = -1, preserving the
behavior of existing tests.

This PR is intentionally limited to introducing partition support in
TestRecord. Follow-up PRs will add multi-partition support to
TopologyTestDriver.

Reviewers: Matthias J. Sax matthias@confluent.io, Lucas Brutschy
lbrutschy@confluent.io

Co-authored-by: Marie-Laure Momplot <marie-laure.momplot@michelin.com>
Co-authored-by: Julien Brunet <julien.brunet2@michelin.com>
Co-authored-by: Adam Souquieres <souquieres.adam@gmail.com>
@github-actions github-actions Bot added streams tests Test fixes (including flaky tests) triage PRs from the community labels Jun 18, 2026

@mjsax mjsax left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks. Made a pass.


/**
* Creates a record.
* Partition defaults to {@code -1} (no explicit partition set).

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should this comment be also on the above constructor?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

this.value = record.value();
this.headers = record.headers();
this.recordTime = Instant.ofEpochMilli(record.timestamp());
this.partition = record.partition();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Technically, there is no guard to what value ConsumerRecord#partition is set -- of course, if we get it from a KafkaConsumer we can expect it to be correctly set. However, new ConsumerRecord is a public API so users could create one manually. Wondering if we should have a guard to check for < 0, and throw(?) or overwrite to -1 (?) for this case, to avoid weird corner cases?

Or would we want to accept that we either crash in < -1 or treat all negative values as "not set" (ie, same as -1)?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

If we choose to throw an exception, I think we could break some exiting tests.
So silent normalization is acceptable, we can overwrite to -1 if the Partition is < 0

this.value = record.value();
this.headers = record.headers();
this.recordTime = Instant.ofEpochMilli(record.timestamp());
this.partition = record.partition() != null ? record.partition() : NO_PARTITION;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Same question -- if record.partition() != null, it could have any value...

For producer value. For the producer case, treating anything negative as invalid and throw could make even more sense compared to ConsumerRecord because here we have the null option.

Thoughts?

@sebastienviale sebastienviale Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

For ProducerRecord, I think it is a little bit different than ConsumerRecord because a ProducerRecord without a partition is certainly a bug from the caller.
In this case we can throw an exception.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just realized that ProducerRecord already has a sentinel and does not allow arbitrary negative values. Therefore, mapping null to NO_PARTITION and preserving non-negative partitions should be sufficient:
So can we just add:
this.partition = record.partition() != null && record.partition() >= 0 ? record.partition() : NO_PARTITION;

}

/**
* Compares this record to {@code o} without considering the {@code partition} field.

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* Compares this record to {@code o} without considering the {@code partition} field.
* Compares this record to {@code otherRecord} without considering the {@code partition} field.

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* assertTrue(expected.equalsIgnorePartition(actual));
* }</pre>
*
* @param o the record to compare against; {@code null} returns {@code false}

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
* @param o the record to compare against; {@code null} returns {@code false}
* @param otherRecord the record to compare against; {@code null} returns {@code false}

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

* @param o the record to compare against; {@code null} returns {@code false}
* @return {@code true} if all fields except {@code partition} are equal
*/
public boolean equalsIgnorePartition(final TestRecord<? extends K, ? extends V> o) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
public boolean equalsIgnorePartition(final TestRecord<? extends K, ? extends V> o) {
public boolean equalsIgnorePartition(final TestRecord<? extends K, ? extends V> otherRecord) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Should it be only TestRecord<K, V> -- during KIP review, I was also briefly thinking if we would want to support sub-types, but in the end, if we compare for equality, a TestRecord with sub-key-type or sub-value-type is not equals to "this" record, so we don't need to support this and can just use <K,V> and disallow at compile time already?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

return o != null && (this == o || equalsFields(o));
}

private boolean equalsFields(final TestRecord<?, ?> other) {

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Suggested change
private boolean equalsFields(final TestRecord<?, ?> other) {
private boolean equalsFields(final TestRecord<K, V> otherRecord) {

@sebastienviale sebastienviale Jun 19, 2026

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I need to keep TestRecord<?, ?> to make the code compile here

@github-actions github-actions Bot removed the triage PRs from the community label Jun 19, 2026
Co-authored-by: Marie-Laure Momplot <marie-laure.momplot@michelin.com>
Co-authored-by: Julien Brunet <julien.brunet2@michelin.com>
Co-authored-by: Adam Souquieres <souquieres.adam@gmail.com>
@sebastienviale

Copy link
Copy Markdown
Contributor Author

Thanks. Made a pass.

@mjsax I took your remarks into consideration

@lucasbru lucasbru left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Two nits about validation, otherwise LGTM!

this.value = value;
this.recordTime = recordTime;
this.headers = new RecordHeaders(headers);
this.partition = partition;

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Maybe validate against negative values? Can I pass the sentinel here? probably not

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good point.
I think we should still allow the NO_PARTITION sentinel (-1), since existing constructors use it to represent an unspecified partition and callers may want to construct such records explicitly.
I'll add validation so that only NO_PARTITION and non-negative partition values are accepted.

this.value = record.value();
this.headers = record.headers();
this.recordTime = Instant.ofEpochMilli(record.timestamp());
this.partition = record.partition() < 0 ? NO_PARTITION : record.partition();

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

ProducerRecord throws on negative partitions, this one silently fixed. Probably both should throw?

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I just updated TestRecord(ProducerRecord). Should both constructors, for ProducerRecord and ConsumerRecord, throw an exception?

@lucasbru

Copy link
Copy Markdown
Member

Could you add a ticket number or MINOR to the PR title?

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Pull request overview

This PR extends TestRecord (Streams test-utils) to carry Kafka partition metadata, enabling future multi-partition testing support in TopologyTestDriver (KIP-1238 prerequisite).

Changes:

  • Add partition field to TestRecord, new constructors to set it explicitly, and accessors.
  • Update equals(), hashCode(), and toString() to include partition; add equalsIgnorePartition() for partition-agnostic assertions.
  • Expand unit tests to cover default/explicit partition behavior and partition-aware equality/toString.

Reviewed changes

Copilot reviewed 2 out of 2 changed files in this pull request and generated 7 comments.

File Description
streams/test-utils/src/main/java/org/apache/kafka/streams/test/TestRecord.java Adds partition state to TestRecord and updates equality/hash/toString plus new comparison helper.
streams/test-utils/src/test/java/org/apache/kafka/streams/test/TestRecordTest.java Updates/extends tests to validate partition defaults, explicit partition, and partition-aware behavior.

💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.

Comment on lines +59 to 65
public TestRecord(final K key, final V value, final Headers headers, final Instant recordTime, final int partition) {
this.key = key;
this.value = value;
this.recordTime = recordTime;
this.headers = new RecordHeaders(headers);
this.partition = partition;
}
Comment on lines +42 to +46
* The partition this record is assigned to.
* A value of {@code -1} is a sentinel meaning "no explicit partition set" and is used
* only on <em>input</em> records created without an explicit partition argument.
* Output records read from {@link org.apache.kafka.streams.TestOutputTopic#readRecordsToList()}
* always carry the real resolved partition ({@code >= 0}).
Comment on lines 162 to +166
this.key = record.key();
this.value = record.value();
this.headers = record.headers();
this.recordTime = Instant.ofEpochMilli(record.timestamp());
this.partition = record.partition() < 0 ? NO_PARTITION : record.partition();
Comment on lines 179 to +187
this.headers = record.headers();
this.recordTime = Instant.ofEpochMilli(record.timestamp());
final Integer partition = record.partition();
if (partition != null && partition < 0) {
throw new IllegalArgumentException(
"Partition must be >= 0 or null, got: " + partition
);
}
this.partition = partition != null ? partition : NO_PARTITION;
* @param otherRecord the record to compare against; {@code null} returns {@code false}
* @return {@code true} if all fields except {@code partition} are equal
*/
public boolean equalsIgnorePartition(final TestRecord<K, V> otherRecord) {
Comment on lines 154 to 159
/**
* Create a {@code TestRecord} from a {@link ConsumerRecord}.
* The partition is taken from {@link ConsumerRecord#partition()}.
*
* @param record The v
*/

Copy link
Copy Markdown
Contributor Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

done

Comment on lines 156 to 160
public void testConsumerRecord() {
final String topicName = "topic";
final ConsumerRecord<String, Integer> consumerRecord = new ConsumerRecord<>(topicName, 1, 0, recordMs,
TimestampType.CREATE_TIME, 0, 0, key, value, headers, Optional.empty());
final TestRecord<String, Integer> testRecord = new TestRecord<>(consumerRecord);
@sebastienviale sebastienviale changed the title Add partition support to TestRecord KAFKA-19871 - Add partition support to TestRecord Jun 19, 2026
sebastienviale and others added 2 commits June 19, 2026 21:17
Co-authored-by: Marie-Laure Momplot <marie-laure.momplot@michelin.com>
Co-authored-by: Julien Brunet <julien.brunet2@michelin.com>
Co-authored-by: Adam Souquieres <souquieres.adam@gmail.com>
Co-authored-by: Marie-Laure Momplot <marie-laure.momplot@michelin.com>
Co-authored-by: Julien Brunet <julien.brunet2@michelin.com>
Co-authored-by: Adam Souquieres <souquieres.adam@gmail.com>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

ci-approved streams tests Test fixes (including flaky tests)

Projects

None yet

Development

Successfully merging this pull request may close these issues.

4 participants